| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- /* @vitest-environment node */
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
- import fs from "node:fs/promises";
- import os from "node:os";
- import path from "node:path";
- import fsp from "node:fs/promises";
- vi.mock("@/lib/auth/session", () => ({
- getSession: vi.fn(),
- }));
- import { getSession } from "@/lib/auth/session";
- import { GET, dynamic, runtime } from "./route.js";
- describe("GET /api/files/[branch]/[year]/[month]/[day]/[filename]", () => {
- let tmpRoot;
- const originalNasRoot = process.env.NAS_ROOT_PATH;
- const paramsOk = {
- branch: "NL01",
- year: "2024",
- month: "10",
- day: "23",
- filename: "test.pdf",
- };
- beforeEach(async () => {
- vi.clearAllMocks();
- tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "api-pdf-"));
- process.env.NAS_ROOT_PATH = tmpRoot;
- const dir = path.join(tmpRoot, "NL01", "2024", "10", "23");
- await fs.mkdir(dir, { recursive: true });
- await fs.writeFile(path.join(dir, "test.pdf"), "dummy-pdf-content");
- });
- afterEach(async () => {
- process.env.NAS_ROOT_PATH = originalNasRoot;
- if (tmpRoot) await fs.rm(tmpRoot, { recursive: true, force: true });
- vi.restoreAllMocks();
- });
- it('exports dynamic="force-dynamic" (RHL-006)', () => {
- expect(dynamic).toBe("force-dynamic");
- });
- it('exports runtime="nodejs" (required for streaming)', () => {
- expect(runtime).toBe("nodejs");
- });
- it("returns 401 when unauthenticated", async () => {
- getSession.mockResolvedValue(null);
- const res = await GET(
- new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"),
- { params: Promise.resolve(paramsOk) }
- );
- expect(res.status).toBe(401);
- expect(await res.json()).toEqual({
- error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
- });
- });
- it("returns 403 when branch access is forbidden", async () => {
- getSession.mockResolvedValue({
- role: "branch",
- branchId: "NL01",
- userId: "u1",
- });
- const res = await GET(
- new Request("http://localhost/api/files/NL02/2024/10/23/test.pdf"),
- {
- params: Promise.resolve({
- ...paramsOk,
- branch: "NL02",
- }),
- }
- );
- expect(res.status).toBe(403);
- expect(await res.json()).toEqual({
- error: { message: "Forbidden", code: "AUTH_FORBIDDEN_BRANCH" },
- });
- });
- it("returns 400 for non-pdf filename", async () => {
- getSession.mockResolvedValue({
- role: "admin",
- branchId: null,
- userId: "u2",
- });
- const res = await GET(
- new Request("http://localhost/api/files/NL01/2024/10/23/test.txt"),
- {
- params: Promise.resolve({
- ...paramsOk,
- filename: "test.txt",
- }),
- }
- );
- expect(res.status).toBe(400);
- expect(await res.json()).toEqual({
- error: {
- message: "Only PDF files are allowed",
- code: "VALIDATION_FILE_EXTENSION",
- details: { filename: "test.txt" },
- },
- });
- });
- it("returns 400 for unsafe filename", async () => {
- getSession.mockResolvedValue({
- role: "admin",
- branchId: null,
- userId: "u2",
- });
- const res = await GET(
- new Request("http://localhost/api/files/NL01/2024/10/23/foo/bar.pdf"),
- {
- params: Promise.resolve({
- ...paramsOk,
- filename: "foo/bar.pdf",
- }),
- }
- );
- expect(res.status).toBe(400);
- expect(await res.json()).toEqual({
- error: {
- message: "Invalid filename parameter",
- code: "VALIDATION_FILENAME",
- details: { filename: "foo/bar.pdf" },
- },
- });
- });
- it("returns 404 when the PDF does not exist (authorized)", async () => {
- getSession.mockResolvedValue({
- role: "admin",
- branchId: null,
- userId: "u2",
- });
- const res = await GET(
- new Request("http://localhost/api/files/NL01/2024/10/23/missing.pdf"),
- {
- params: Promise.resolve({
- ...paramsOk,
- filename: "missing.pdf",
- }),
- }
- );
- expect(res.status).toBe(404);
- expect(await res.json()).toEqual({
- error: {
- message: "Not found",
- code: "FS_NOT_FOUND",
- details: {
- branch: "NL01",
- year: "2024",
- month: "10",
- day: "23",
- filename: "missing.pdf",
- },
- },
- });
- });
- it("returns 500 for other filesystem errors (mocked)", async () => {
- getSession.mockResolvedValue({
- role: "admin",
- branchId: null,
- userId: "u2",
- });
- const spy = vi
- .spyOn(fsp, "stat")
- .mockRejectedValue(
- Object.assign(new Error("EACCES"), { code: "EACCES" })
- );
- const res = await GET(
- new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"),
- { params: Promise.resolve(paramsOk) }
- );
- expect(res.status).toBe(500);
- expect(await res.json()).toEqual({
- error: { message: "Internal server error", code: "FS_STORAGE_ERROR" },
- });
- spy.mockRestore();
- });
- it("streams the PDF with inline Content-Disposition by default", async () => {
- getSession.mockResolvedValue({
- role: "admin",
- branchId: null,
- userId: "u2",
- });
- const res = await GET(
- new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"),
- { params: Promise.resolve(paramsOk) }
- );
- expect(res.status).toBe(200);
- expect(res.headers.get("Content-Type")).toBe("application/pdf");
- expect(res.headers.get("Cache-Control")).toBe("no-store");
- expect(res.headers.get("Content-Disposition")).toBe(
- 'inline; filename="test.pdf"'
- );
- const buf = await res.arrayBuffer();
- expect(Buffer.from(buf).toString("utf8")).toBe("dummy-pdf-content");
- });
- it("uses attachment disposition when download=1", async () => {
- getSession.mockResolvedValue({
- role: "admin",
- branchId: null,
- userId: "u2",
- });
- const res = await GET(
- new Request(
- "http://localhost/api/files/NL01/2024/10/23/test.pdf?download=1"
- ),
- { params: Promise.resolve(paramsOk) }
- );
- expect(res.status).toBe(200);
- expect(res.headers.get("Content-Disposition")).toBe(
- 'attachment; filename="test.pdf"'
- );
- });
- });
|